#!/usr/bin/env python
#
# poller.py
#
# Version : 0.6
# Date : 20080424
#
# Author : Bernard Czenkusz
# Email  : bernie@skipole.co.uk

#
# poller.py - poller engine, run as part of skipolemonitor
# Copyright (c) 2007 Bernard Czenkusz
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#

"""The main engine of Skipole Monitor, poller_loop() is run from monitor.py to poll the network

For each group of hosts, it calls the pinger function in the pinger module,
which does the pings and reports results back.

When the polls are completed, the function refresh_html() calls the module makehtml to
update the web pages.
The function send_emails() sends email alerts.
Calls sm_syslog.send_syslogs() to send syslog alerts.
"""

import os
import shutil
import sys
import time
import pinger
import socket
import makehtml
import smtplib
import sm_syslog
import graphs
from net import *



# mail is only sent if there is less than a maximum already sent in any 24 hours
# so start off with a global mail timestamp, and a global mail count
mail_timestamp=time.time()
mail_count=0

# global timestamp of last poll
_LAST = 0


def get_time_delta():
    """Return the current time, and the number of seconds between the
       beginning of the last poll interval and now"""
    time_now = int(time.time())
    poll_interval = int(net_cfg["POLL_INTERVAL"])
    return time_now, time_now - (time_now // poll_interval)*poll_interval

def poller_loop():
    """Loops until the PROGRAM_SHUTDOWN flag is set, and initiates
       network polls at net_cfg["POLL_INTERVAL"] time intervals.""" 
    global _LAST
    global PROGRAM_SHUTDOWN
    #
    # doing polling loop
    #
    while not PROGRAM_SHUTDOWN:
        # If a network reload is in progress, don't poll
        if net_reload():
            time.sleep(1)
            continue
        #
        # Get the current time and the time since the start of this poll interval
        time_now, time_delta = get_time_delta()
        #
        # Check that time_now and _LAST are valid, if time_now is actually earlier
        # than _LAST, possibly the computer clock has changed. so delete any previouse
        # logs to stop confusion
        if time_now < _LAST :
            del_logs()
            # And start from scratch
            _LAST = time_now
            time.sleep(1)
            continue
        #
        # if the time since the last update is less than the delta time
        # then the poll has been done for this time interval so continue
        if (time_now - _LAST) <= time_delta:
            # While waiting, check if a network refresh is requested
            if flag_html_refresh():
                refresh_html()
            time.sleep(1)
            continue
        #
        # update the _LAST variable
        _LAST = time_now
        # so get polling
        poll()
        # Check program has not been shutdown
        if PROGRAM_SHUTDOWN: break
        # update all html pages and send any emails required
        refresh_html()
        # Check program has not been shutdown
        if PROGRAM_SHUTDOWN: break
        # and wait at least 2 seconds between polls
        time.sleep(1)
        if PROGRAM_SHUTDOWN: break
        time.sleep(1)
    return

def poll():
    """Iterate through all network items to find each group, then ping all the hosts in a group"""
    for id in network(0):
        # Checking groups only, if its a host, skip to next
        if is_net_group(id):
            # So its a group, create the lists of hosts_in_group
            hosts_in_group=host_check(id)
            if hosts_in_group != []:
                # poll each of the hosts, setting their status
                host_poll(hosts_in_group)
    return


def host_check(group_id):
    """Return a list of hosts within the group"""
    hosts_in_group=[]
    for id in network(group_id):
        # Checking hosts only, if its a group, skip to next
        if is_net_group(id) : continue
        # If the host is not in this group, skip to next
        if get_net(id, PARENT) !=  group_id : continue
        # So this is a host in this group.
        hosts_in_group.append(id)
        # Set the group status appropriately
        if get_net(id, STATUS)>get_net(group_id, STATUS):
            set_net(group_id, STATUS, get_net(id, STATUS))
    return hosts_in_group


def host_poll(poll_list):
    """Call pinger to ping each host in the poll_list"""
    # if a network reload is in progress, just return
    if net_reload(): return
    if PROGRAM_SHUTDOWN: return
    # given a list of hosts, poll them, and fill in logs
    list_of_hosts=[]
    for id in poll_list:
        # Save its id, and ip address in a list,
        # then when the list is full, pass it to pinger
        list_of_hosts.append((id, get_net(id, ADDRESS)))
    # must now pass this to pinger, and when get the result, must
    # update the status of hosts accordingly
    ping_results=pinger.pinger(list_of_hosts)
    # If a network reload is in progress, just return
    if net_reload(): return
    if PROGRAM_SHUTDOWN: return
    for host in ping_results:
        # for each host
        # host[0] is the host id
        # host[1] is the number of successfull pings to this host
        # host[2] is the average RTT of pings to this host
        #
        # log the result
        log(host)
        #
        # Set host status
        # Status; 0:white, 1:green, 2:yellow, 3:red
        status=3
        if host[1]>0 : status=2
        if host[1]>2 : status=1
        if get_net(host[0], STATUS) != status:
            set_net(host[0], STATUS, status)
    return


def del_logs():
    """The poller loop checks if there is a peculiarity, such as the computer clock
       changing. If so, this function deletes any previous logs to stop confusion"""
    for id in network(0):
        if is_net_host(id):
            data_file=os.path.join(net_cfg["NETWORK_DATA_FOLDER"],str(id),'data.cvs')
            temp_file=os.path.join(net_cfg["NETWORK_DATA_FOLDER"],str(id),'data.tmp')
            if os.path.exists(data_file):
                os.remove(data_file)
            if os.path.exists(temp_file):
                os.remove(temp_file)
    return

    
def log(host):
    """Within the data folder, update log.txt and data.csv files."""
    # host[0] is the host id
    # host[1] is the number of successfull pings to this host
    # host[2] is the average RTT of pings to this host
    #
    # Update the graphs database data.csv for this host
    graphs.update(host)
    #
    host_data_folder=os.path.join(net_cfg["NETWORK_DATA_FOLDER"],str(host[0]))
    #
    # filepath is the name of the log file, held in the host data folder
    filepath=os.path.join(host_data_folder,"log.txt")
    tempfile=os.path.join(host_data_folder,"log.tmp")
    #
    if host[1] == 0:
        line_to_add=time.asctime() + " : Replies = 0 : milliseconds RTT = --\n"
    else:
        line_to_add=time.asctime() + " : Replies = " + str(host[1]) + " : milliseconds RTT = " + str(host[2]) + "\n"
    # Create file log.tmp and insert this line into it
    tempf=open(tempfile, 'w')
    tempf.write(line_to_add)
    # read lines from file log.txt, and add them to log.tmp
    maxlines=int(net_cfg["LOG_RECORDS"])
    if os.path.exists(filepath) and maxlines > 1:
        # read LOG_RECORDS lines from file
        fp=open(filepath, 'r')
        linecount=2
        for line in fp:
            tempf.write(line)
            linecount += 1
            if linecount > maxlines:
                break
        fp.close()
    tempf.close()
    # Delete original log.txt
    if os.path.exists(filepath):
        os.remove(filepath)
    # Rename log.tmp to log.txt
    os.rename(tempfile, filepath)
    # log.txt now contains the logged data
    return

def subgroup_check(group_id):
    """Return a list of sub-groups within group_id"""
    subgroups_in_this_group=[]
    for id in network(group_id):
        # Checking groups only, if its a host, skip to next
        if is_net_host(id) : continue
        # If the sub-group is not in this group, skip to next
        if get_net(id, PARENT) !=  group_id : continue
        # So this is a sub-group in this group. Save its id in a list,
        subgroups_in_this_group.append(id)
        # Set the parent group status appropriately
        if get_net(id, STATUS)>get_net(group_id, STATUS) : set_net(group_id, STATUS, get_net(id, STATUS))
    # and return the list of sub-group id's in this group
    return subgroups_in_this_group


def refresh_html():
    """Refresh the html pages"""
    # If a network reload is in progress, or a shutdown requested, just return
    if net_reload(): return
    if PROGRAM_SHUTDOWN: return
    all_alerts_list=[]
    # iterate through all devices to find each group
    #
    # Have to iterate through items three times:
    #   once to set the status of all groups,
    #   once to create the html pages, as status is now known
    #   once to draw the log files and images
    #
    # log files and images are not done in the same iteration
    # as the creation of html pages - as that would slow the
    # page creation down
    #
    #  Set all group status
    #
    for id in network(0):
        # Checking groups
        if is_net_group(id):
            # Its a group : Set its initial status to zero
            set_net(id, STATUS, 0)
            # Create a list of hosts in this group, and update group status
            hosts_in_group=host_check(id)
            # Create a list of the subgroups in this groups, and update group status
            subgroups_in_group=subgroup_check(id)
    #
    # set All Alerts group (id 0) to have the same status as the top group (id 1)
    set_net(0, STATUS, get_net(1, STATUS))
    #
    #  Create the html
    #
    # If a network reload is in progress, just return
    if net_reload(): return
    if PROGRAM_SHUTDOWN: return
    #
    for id in network(0):
        # Checking groups
        if is_net_group(id):
            # Its a group : Create a list of hosts in this group
            hosts_in_group=host_check(id)
            # Create a list of the subgroups in this group
            subgroups_in_group=subgroup_check(id)
            # Create a html group page
            makehtml.makehtml(id, subgroups_in_group, hosts_in_group)
        else:
            # Its a host
            # If there's an alert, the host needs to be shown on the all_alerts page
            if get_net(id, STATUS)>1 : all_alerts_list.append(id)
            # Create the html host page
            makehtml.host_page(id)
    #
    # Create the All Alerts page
    makehtml.makealertspage(all_alerts_list)
    # And if any email alerts should be sent, send them
    send_emails()
    # And if any syslog alerts should be sent, send them
    sm_syslog.send_syslogs()
    #
    # Get new counts of host status
    host_count=0
    red_count=0
    yellow_count=0
    green_count=0
    # for each item, set OLDSTATUS to the current status
    # and update host log html file and graph image
    # 
    for id in network(0):
        set_net(id, OLDSTATUS, get_net(id, STATUS))
        # for each host, update the log html file and image
        # Checking hosts only
        if is_net_host(id):
            # Increment the host count
            host_count += 1
            # Create the html log file
            makehtml.make_log_html(id)
            # Draw graphs
            graphs.drawgraphs(id)
            #
            if get_net(id, STATUS) == 3: red_count += 1
            if get_net(id, STATUS) == 2: yellow_count += 1
            if get_net(id, STATUS) == 1: green_count += 1
    HOST_STATS[1]=red_count
    HOST_STATS[2]=yellow_count
    HOST_STATS[3]=green_count
    HOST_STATS[0]=host_count
    # And unset the HTML_REFRESH flag as the network has just been refreshed
    unset_flag_html_refresh()
    return


def send_emails():
    """If email alerting is enabled, send any necessary alerts"""
    global mail_count
    global mail_timestamp
    global net_cfg
    # Check if email is globally enabled.
    if net_cfg["EMAIL_ENABLED"] == "0" : return
    if net_cfg["EMAIL_MAX_RATE"] == "0" : return
    if net_cfg["SMTP_SERVER"] == "not.set" : return
    # Check SMTP server name is resolvable
    try:
        socket.gethostbyname(net_cfg["SMTP_SERVER"])
    except:
        return
    #
    # Only send an email, if less than the max rate has been sent in the last 24 hours
    elapsed = int(time.time()-mail_timestamp)
    if elapsed > 86400 :
        # 24 hours have elapsed, so can start again
        mail_timestamp=time.time()
        mail_count=0
    if mail_count >= int(net_cfg["EMAIL_MAX_RATE"]): return
    if mail_count >= 300: return
    # So in the last 24 hours, less than the max rate have been sent, so can send another one
    # 300 is a further limit, set here in case the entry in the config file is set incorrectly
    #
    mail_contents=""
    # for each item, if its status has changed, then send an alert
    for id in network(0):
        new_status=get_net(id, STATUS)
        old_status=get_net(id, OLDSTATUS)
        if new_status != old_status:
            # If email alerts not enabled for this id
            if not get_net(id, EMAIL): continue
            # if the status change is from white (unknown) to green (ok) - don't send email
            if old_status==0 and new_status==1: continue
            # Email alert enabled, so add to body
            if get_net(id, ADDRESS) == "0":
                mail_contents += "Status of group %s \r\n" % get_net(id, NAME)
            else:
                mail_contents += "Status of host %s : %s \r\n" % (get_net(id, NAME), get_net(id, ADDRESS))
            mail_contents += "Description : %s \r\n" % get_net(id, DESCRIPTION)
            if old_status == 0:
                mail_contents += "has changed from unknown (White) to "
            if old_status == 1:
                mail_contents += "has changed from OK (Green) to "
            if old_status == 2:
                mail_contents += "has changed from partial failure (Yellow) to "
            if old_status == 3:
                mail_contents += "has changed from Fail (Red) to "
            if new_status == 0:
                mail_contents += "unknown (White)\r\n\r\n"
            if new_status == 1:
                mail_contents += "OK (Green)\r\n\r\n"
            if new_status == 2:
                mail_contents += "partial failure (Yellow)\r\n\r\n"
            if new_status == 3:
                mail_contents += "Fail (Red)\r\n\r\n"
    # Mail contents finished
    # If there has been nothing to send, so contents are blank, just return
    if mail_contents == "": return
    # So we are to send an email, increment the mail count
    mail_count += 1
    # Create the email to send
    header_text="From: "+net_cfg["EMAIL_SOURCE"]+"\r\n"
    header_text+="To: "+net_cfg["EMAIL_DESTINATION"]+"\r\n"
    header_text+="Subject: "+net_cfg["EMAIL_SUBJECT"]+"\r\n\r\n"
    header_text+="Alert from Skipole Monitor : "+time.asctime()+"\r\n\r\n"
    # Create a list of addresses to send the alert to
    address_list=net_cfg["EMAIL_DESTINATION"].split(";")
    #
    # And send the mail
    try:
        outbox=smtplib.SMTP(net_cfg["SMTP_SERVER"])
        outbox.sendmail(net_cfg["EMAIL_SOURCE"], address_list, header_text+mail_contents)
        outbox.quit()
    except:
        # If mail fails, dont create errors, carry on with rest of
        # the program, accepting this part won't work
        pass
    return

